import asyncio
import random
import os
import time
import itertools


async def make_item(size=5) -> str:
    return os.urandom(size).hex()


async def rand_sleep(caller: str) -> None:
    i = random.randint(0, 3)
    print(f"Caller '{caller}' sleeps for {i} second(s) ...")
    await asyncio.sleep(i)


async def producer(name: int, q: asyncio.Queue) -> None:
    tag = f"Producer {name}"
    n = random.randint(1, 3)
    print(f"{tag} started with n = {n}")
    for _ in range(n):
    # for _ in itertools.repeat(None, n):
        await rand_sleep(tag)
        product = await make_item()
        psec = time.perf_counter()
        await q.put((product, psec))
        print(f"{tag} put {product} into queue.")


async def consumer(name: int, q: asyncio.Queue) -> None:
    tag = f"Consumer {name}"
    while True:
        await rand_sleep(tag)
        product, psec = await q.get()
        psec_now = time.perf_counter()
        print(f"{tag} got product {product} in {psec_now - psec:.4f} second(s).")
        # await q.task_done()  # IMPORTANT await here cause dead-lock, I don't know why.
        q.task_done()


async def main(n_prod=3, n_cons=3):
    q = asyncio.Queue()
    consumers = [asyncio.create_task(consumer(i, q)) for i in range(n_cons)]
    await asyncio.gather(*[asyncio.create_task(producer(i, q)) for i in range(n_prod)])
    print('q joining ...')
    await q.join()
    print('q joined.')
    print('c cancelling ...')
    for c in consumers:
        c.cancel()
    print('c all cancelled.')

if '__main__' == __name__:
    random.seed(8888)
    asyncio.run(main())
